'''This module provides the central engine classes and services used for loading and
managing games.
'''
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division

import logging
import argparse
import time
import weakref
import io
import os
from os import path

from kharlia.core import (NativeClass, ResolutionError,
                          GetApp, FindGlobal, FindGlobalF, LoadConfig, CreateTasklet,
                          CObject, CSubsystem, CEvent, CService)
from kharlia.engine.world import CWorld, CGame, CLocalPlayer
from kharlia.engine.client import FTexture, CSpriteAniDef, CClient

from kharlia._engine import CEngineBase

__all__ = [
    'CEngine',
    'CGameEngine',
    'CEngineService',
    ]

GLogger = logging.getLogger('engine')

@NativeClass
class CEngine(CEngineBase, CSubsystem):
    '''The base game engine class.'''
    def __init__(self):
        self.__initnative__()
        CSubsystem.__init__(self)
        self.ResourcePaths = []

    @property
    def EditorSelection(self):
        return self._EditorSelection
    
    @EditorSelection.setter
    def EditorSelection(self, value):
        if value is None:
            self._EditorSelection = FRectF()
        else:
            self._EditorSelection = value

    def Init(self):
        # Perform remaining class initialization
        from kharlia.engine import actor, level, world
        actor.InitComponentClasses()
        world.CWorld.SetLevelClass(level.CLevel)
        level.CLevel.SetPlayerControllerClass(actor.APlayerController)
        level.CLevel.SetActorClass(actor.CActor)
         
        #-- Initialize the engine
        self.Config = GetApp().ConfigRoot['Engine']['Engine']
        self.ClientTickRate = self.Config['ClientTickRate']
        paths = self.Config['ResourcePaths']
        paths = map(lambda x: (int(x[0]), x[1]), paths.iteritems())
        paths.sort(lambda x, y: cmp(x[0], y[0]))
        paths = map(lambda x: x[1], paths)
        paths = map(path.normpath, paths)
        self.ResourcePaths.extend(paths)

        GLogger.info('resource paths:')
        for p in self.ResourcePaths:
            GLogger.info('  %s', p)

        super(CEngine, self).Init()

    def ReportTickTime(self, time):
        GLogger.debug('reporting tick time: %s', time)

    def Exit(self):
        self.TestActor = None
        self.TestMovement = None
        super(CEngine, self).Exit()

    def GetClientClass(self):
        '''Return the desired client class. Called from InitClient().'''
        return CClient

    def GetLocalPlayerClass(self):
        '''Return the desired local player class. Called from Init().'''
        return CLocalPlayer

    def LoadResources(self, resources):
        '''
        Load a sequence of resources. Return list of resources that couldn't be loaded.
        
        This is primarily used to load resources referenced after a level has been
        unpickled.
        '''
        fails = []
        #print('beginning resource load')
        for r in resources:
            #print('loading resource:', r)
            if not r.IsLoaded:
                if r.CanLoad:
                    try:
                        r.Load()
                    except Exception:
                        fails.append(r)
                    #print('loaded resource:', r)
                else:
                    fails.append(r)
        #print('end resource load')
        return fails
    
    def CreateAndLoadResources(self, resources):
        create = self.ResourceManager.CreateResource
        resources = {}
        for rtype, rname in resources:
            resources[rname] = create(rtype, rname)
        fails = self.LoadResources(resources.itervalues())
        return resources, fails

    def ResolvePath(self, fpath, exc=False):
        '''Attempts to locate the file specified with fpath in one of the
        resource paths. Returns None if not found.'''
        for p in self.ResourcePaths:
            full = path.join(p, fpath)
            if path.exists(full):
                full = path.normpath(full)
                GLogger.debug('resolved resource path: %s in %s', fpath, p)
                return full
        GLogger.debug('failed to resolve resource path: %s', fpath)
        if exc:
            raise ValueError('failed to resolve resource path: {}'.format(fpath))
        return None
     
    def Exec(self, cmd, out=print):
        '''
        Command execution entry point.
        
        This should be called directly by the client's console to execute any commands.
        '''
        assert out is not None
        assert len(cmd) >= 1
        
        if cmd[0] == 'test':
            out('Test 123!')
            return True
        
        if cmd[0] == 'mro':
            if len(cmd) >= 2:
                try:
                    ob = FindGlobal(cmd[1])
                    if ob is not None and isinstance(ob, type):
                        out('MRO for class ' + cmd[1] + ':')
                        for cls in ob.mro():
                            out('  ' + str(cls))  
                except ResolutionError:
                    out('Class not found.')
            else:
                out('Must specify global class name.')
            return True
        
        if cmd[0] == 'exit':
            GetApp().RequestExit()
            return True

        if cmd[0] == 'fexit':
            GetApp().ForceExit()
            return True

        if cmd[0] == 'getg':
            if len(cmd) < 2:
                out('Must specify global name.')
            else:
                var, err = FindGlobalF(cmd[1])
                if not err:
                    out('{0}: {1}'.format(cmd[1], var))
                else:
                    out('Global not found: {0}'.format(cmd[1]))
            return True

        # Pass to the client
        if self.Client and self.Client.Exec(cmd, out):
            return True

        # Pass to the local player controller
        act = self.LocalPlayer.Actor
        pc = act.PlayerController if act else None
        if pc and pc.Exec(cmd, out):
            return True
            
        return False
    
    # Key events called when the client receives input
    def OnKeyDown(self, keycode):
        '''Called by client when it receives a key event.'''
        a = self.LocalPlayer.Actor
        if a:
            a.SendMessage('InputKeyDown', keycode)
        
    def OnKeyUp(self, keycode):
        '''Called by client when it receives a key event.'''
        a = self.LocalPlayer.Actor
        if a:
            a.SendMessage('InputKeyUp', keycode)
        
    def OnChar(self, codepoint):
        '''Called by client when it receives a key event.'''
        a = self.LocalPlayer.Actor
        if a:
            a.SendMessage('InputChar', codepoint)

    def OnViewportActivate(self, viewport, active):
        '''Called by client when its viewport is activated/deactivated.'''
        a = self.LocalPlayer.Actor
        if a:
            a.SendMessage('ViewportActivate', viewport, active)

class CGameEngine(CEngine):
    '''The game-specific engine subclass.'''
    def __init__(self):
        CEngine.__init__(self)
        self.SavePath = None
        self.SaveFileFormat = 'Save{}.ksv'

    def Init(self):
        '''Initialize the game engine.'''
        app = GetApp()
        wxsvc = app.ServiceManager.GetServiceNoFail('kharlia.wxinter.CWXService')
        assert wxsvc.IsStarted

        # Parse any command line arguments
        # TODO: Organize what systems own what arguments
        parser = argparse.ArgumentParser(prefix_chars='-+', add_help=False)
        parser.add_argument('mode', nargs='?', metavar='MODE', type=unicode,
                            default=None)
        parser.add_argument('+l', '++load', type=unicode, default='autoload.klv')
        parser.add_argument('+h', '++host', type=unicode, default=None)
        parser.add_argument('++port', type=int, default=60500)
        args, app.Args = parser.parse_known_args(app.Args)
        if args.mode:
            args.mode = args.mode.lower()

        # MODES:
        #   default: start the client and load a singleplayer map
        #   connect: connect to a network game specified by ++host and ++port
        #            as a client
        #   listen: start the client, load a map, and listen for connections
        #           on ++port
        #   server: open a console window, load a map, and listen for connections
        #           on ++port

        # Initialize the base engine
        CEngine.Init(self)
        self.SavePath = self.Config.get('SavePath', 'saves')
        
        # Initialize the client
        # TODO: Make it so game still runs properly if client is started after
        #       the level is loaded
        if args.mode != 'server':
            # Create and set the top wxPython window with the basic frame class.
            def frame_close_callback():
                app.RequestExit()
                return True

            framecls = FindGlobal(self.Config['ViewportFrameClass'])
            frame = wxsvc.CreateTopWindow(
                framecls,
                app.Name,
                (self.Config['Width'], self.Config['Height']),
                frame_close_callback
            )
            if not hasattr(frame, 'Viewport'):
                raise TypeError('Frame instance does not define a viewport accessor')
        
            # Initialize the client now that we have a viewport.
            # This is necessary because an OpenGL context must be created
            # for CEGUI to be initialized in the client. The client 
            # requires a valid target window to initialize the OpenGL
            # context, which the viewport provides.
            try:
                self.InitClient(frame.Viewport)
            except Exception:
                GLogger.error('client init failed, closing viewport')
                self.ExitClient()
                frame.Close()
                raise

        # Start a game or connect to one
        if args.mode == 'connect':
            host = args.host
            port = args.port
            if not host:
                raise ValueError('++host required for connection')
            self.Connect(str(host), port)
            GLogger.info('Connecting to %s:%i', host, port)
        else:
            self.PlayWorld(args.load)

        # Start the server
        if args.mode == 'server' or args.mode == 'listen':
            # no port option yet
            self.World.NetListen()

        #self.LoadResources(self.ResourceManager)

    def Exit(self):
        CEngine.Exit(self)

    def Exec(self, cmd, out=print):
        assert out is not None
        assert len(cmd) >= 1
        cmd = map(unicode, cmd)

        if cmd[0] == 'playworld':
            if len(cmd) >= 2:
                filename = cmd[1]
                self.PlayWorld(filename)
            else:
                out('Most provide filename for world or level.')
            return True
        else:
            return CEngine.Exec(self, cmd, out)

    def SetupLocalPlayerCamera(self):
        '''
        Set the client camera to the local player's camera component.

        Returns bool representing success.
        '''
        if self.LocalPlayer.Actor and self.Client:
            pc = self.LocalPlayer.Actor.PlayerController
            if pc and pc.Camera:
                self.Client.Camera = pc.Camera
                return True
        return False

    def LoadWorldDef(self, name):
        '''Load a world definition file (*.kwd).

        Returns the world with game class set, all levels added, and resources
        loaded.
        '''
        GLogger.info('Loading world from def: %s', name)
        start = time.clock()

        # Worlds are defined using config file
        wdef = LoadConfig(name)
        wname = wdef['Name']
        gameclsname = wdef['GameClass']
        levels = wdef['Levels']

        GLogger.info('Game class name: %s', gameclsname)
        gamecls = FindGlobal(gameclsname) if gameclsname else None

        world = CWorld()
        world.GameClass = gamecls

        # Now load the level file names, relative to the world definition
        nhead, ntail = path.split(name)
        for lname in levels.itervalues():
            lpath = path.join(nhead, lname)
            GLogger.info('loading world level: %s', lpath)
            with io.open(lpath, 'rb') as f:
                level, resources = CWorld.LoadLevelPackage(f, self.ResourceManager)
            world.AddLevel(level)
            self.LoadResources(resources)

        elapsed = time.clock() - start
        GLogger.info('Loaded world definition in %s seconds', elapsed)

        return world

    def LoadSingleLevel(self, name):
        '''Load a single level file. Creates a default world to hold it.'''
        GLogger.info('Loading single level: %s', name)
        with io.open(name, 'rb') as f:
            level, resources = world.CWorld.LoadLevelPackage(f, self.ResourceManager)
        world = CWorld()
        world.AddLevel(level)
        self.LoadResources(resources)
        return world

    def PlayWorld(self, name=None, nogame=False):
        assert not self.World
        assert isinstance(name, basestring)

        name = self.ResolvePath(name, exc=True)
        
        if not path.isfile(name):
            raise ValueError('must specify a valid file path')
        if not path.exists(name):
            raise ValueError('file path does not exist')

        nroot, ext = path.splitext(name)
        ext = ext.lower()
        gamecls = None

        # For now we rely on an accurate extension to
        # determine how to handle the file.
        if ext == '.klv':
            world = self.LoadSingleLevel(name)
        elif ext == '.kwd':
            world = self.LoadWorldDef(name)
        elif ext == '.tmx':
            world = self.LoadSingleLevel(nroot + '.klv')
        else:
            raise ValueError('unknown file type: ' + ext)

        self.World = world
        if nogame:
            return
        
        # Game class could be set by one of the loader functions. Normally
        # it should be specified in the world definition file.
        if not world.GameClass:
            gameclsname = GetApp().ConfigRoot['Engine']['Game']['DefaultClass']
            gamecls = FindGlobal(gameclsname)
            world.GameClass = gamecls
        assert issubclass(world.GameClass, CGame)
        world.StartGame()

        # If the client is open, the local player needs to enter the world
        if self.Client:
            #world.CreatePlayerActor(self.LocalPlayer)
            world.AddPlayer(self.LocalPlayer)
            #self.SetupLocalPlayerCamera()

    def SaveGame(self, position=0):
        assert isinstance(position, int)
        assert self.World and self.World.Game, 'game must exist'
        GLogger.debug('saving game to position %s', position)

        self.World.Game.IsSaveGame = True
        if not os.access(self.SavePath, os.F_OK):
            os.mkdir(self.SavePath)

        filename = path.join(self.SavePath, self.SaveFileFormat.format(position))
        GLogger.debug('saving to: %s', filename)
        with io.open(filename, 'wb') as f:
            CWorld.SaveWorldPackage(self.World, f)
        GLogger.debug('finished save')

    def LoadSavedGame(self, position=0):
        assert not self.World
        GLogger.info('loading game from position %s', position)

        filename = path.join(self.SavePath, self.SaveFileFormat.format(position))
        GLogger.debug('loading from: %s', filename)
        with io.open(filename, 'rb') as f:
            world, resources = CWorld.LoadWorldPackage(f, self.ResourceManager)
        fails = self.LoadResources(resources)
        if len(fails) > 0:
            raise RuntimeError('resource load failures while loading saved game')
        self.World = world

        if self.Client:
            assert not self.LocalPlayer.Actor
            world.AddPlayer(self.LocalPlayer)

        GLogger.info('loaded game from position %s', position)


class CEngineService(CService):
    '''
    Manages the initialization and linking of the game engine.
    '''
    Name = 'Game Engine Service'
    Depends = ['kharlia.wxinter.CWXService']
    
    def __init__(self, *args, **kwds):
        CService.__init__(self, *args, **kwds)
        self._Engine = None
        self.EngineTicker = None
    
    @property
    def Engine(self):
        return self._Engine
    
    @property
    def IsStarted(self):
        return self._IsStarted and self._Engine
    
    def OnStart(self):
        '''
        Create and initialize the engine.
        '''
        global GLogger

        #GLogger = logging.getLogger('engine')

        app = self.Manager.App
        cfg = app.ConfigRoot['Engine']['Engine']

        clsname = cfg.get('ClassName', 'kharlia.engine.main.CGameEngine')
        ssindex = cfg.get('SubsystemIndex', 10)
        cls = FindGlobal(clsname)
        if not issubclass(cls, CEngine):
            raise TypeError('Engine class must be CEngine or a subclass')

        eng = cls()
        try:
            eng.Init()
        except Exception as ex:
            eng.Exit()
            raise
        #app.AddSubsystem(ssindex, eng)
        self.EngineTicker = CreateTasklet(eng.TaskletMain)()
        self._Engine = eng

        GLogger.debug('initialized the engine')
    
    def OnStop(self):
        global GLogger
        
        #self.Manager.App.RemoveSubsystem(self._Engine)
        if self.EngineTicker:
            self.EngineTicker.kill()
            self.EngineTicker = None
        self._Engine.Exit()
        self._Engine = None
        GLogger.debug('exited the engine')
        #GLogger = None